package math;

import java.util.*;
import java.util.function.*;

public class SymbolTable
{
    private final Map<String,Symbol> table;

    static final SymbolTable DEFAULT_TABLE = new SymbolTable(true/*unused*/);

    private SymbolTable(boolean defaultInit/*unused parameter*/)
    {
        this();

        register("e",Constant.E);
        register("pi",Constant.PI);
        register("infty",Constant.INFINITY);
        register("i",Constant.I);

        //register("frac",Function.builder().
                //necessary(2).parameter(List.of("x","y")).
                    //body((scope,arg) -> {
                        //return arg.get(0).evaluate(scope) / arg.get(1).evaluate(scope);
                    //}).build());
        //register("sqrt",Function.builder()
                //.necessary(1).optional(1).parameter(List.of("y","x"))
                    //.defaultValue(List.of(new NumberNode(2)))
                    //.body((scope,arg) -> {
                        //return Math.pow(arg.get(0).evaluate(scope),arg.size() == 2 ? (1.0 / arg.get(1).evaluate(scope)) : (1.0 / 2.0))
                    //}).build());
        //register("sum",SymbolType.FUNCTION,0,2);
        //register("prod",SymbolType.FUNCTION,0,2);
        //register("sin",SymbolType.FUNCTION,0,1);
        //register("cos",SymbolType.FUNCTION,0,1);
        //register("tan",SymbolType.FUNCTION,0,1);
        //register("csc",SymbolType.FUNCTION,0,1);
        //register("sec",SymbolType.FUNCTION,0,1);
        //register("cot",SymbolType.FUNCTION,0,1);
        //register("arcsin",SymbolType.FUNCTION,0,1);
        //register("arccos",SymbolType.FUNCTION,0,1);
        //register("arctan",SymbolType.FUNCTION,0,1);
        //register("log",SymbolType.FUNCTION,1,1);
        //register("ln",SymbolType.FUNCTION,0,1);

    }

    SymbolTable()
    {
        this.table = new HashMap<>();
    }

    SymbolTable register(String symbol,Symbol content)
    {
        table.put(symbol,content);
        return this;
    }

    public Symbol get(String s)
    {
        return table.get(s);
    }

    public boolean contains(String s)
    {
        return table.containsKey(s);
    }

    public static SymbolTable getDefaultTable()
    {
        return new SymbolTable(true/*unused*/);
    }
}

abstract class Symbol 
{
    String symbol;
    
    Symbol(String symbol)
    {
        this.symbol = symbol;
    }

    public String getSymbol()
    {
        return symbol;
    }
}

class Variable extends Symbol
{
    double value;

    public Variable(String symbol,double value)
    {
        super(symbol);
        this.value = value;
    }

    public double getValue()
    {
        return this.value;
    }

    public double setValue(double v)
    {
        double old = value;
        value = v;
        return old;
    }

    @Override
    public String toString()
    {
        return getClass().getName() + "<value=" + value + ">";
    }

}

class Function extends Symbol
{
    int optionalArgumentCount;
    List<MathNode> optionalArgumentDefaultValue;

    int necessaryArgumentCount;

    MathNode body;

    List<String> parameters;

    private Function(String symbol,int oac,int nac,List<MathNode> oadv,List<String> param,MathNode body)
    {
        super(symbol);
        if(oac != oadv.size())
        {
            throw new IllegalArgumentException("Dismatched optional argument count: " + toString());
        }

        this.optionalArgumentCount = oac;
        this.necessaryArgumentCount = nac;
        this.optionalArgumentDefaultValue = oadv;
        this.parameters = param;
        this.body = body;
    }

    public int getOptionalArgumentCount()
    {
        return this.optionalArgumentCount;
    }

    public int getNecessaryArgumentCount()
    {
        return this.necessaryArgumentCount;
    }

    @Override
    public String toString()
    {
        return getClass().getName() + "<symbol=" + getSymbol() + ", body=" + body + ", optionalArgumentCount=" + optionalArgumentCount + ", necessaryArgumentCount=" + necessaryArgumentCount + ", optionalArgumentDefaultValue=" + optionalArgumentDefaultValue + ", parameters=" + parameters + ">";
    }

    @Override
    public String getSymbol()
    {
        StringBuilder builder = new StringBuilder(super.getSymbol());
        builder.append("(");

        if(necessaryArgumentCount == 0 && optionalArgumentCount == 0)
        {
            builder.append(")");
            return builder.toString();
        }

        for(int i = 0;i < necessaryArgumentCount;i++)
        {
            if(i > 0)
            {
                builder.append(",");
            }
            builder.append(parameters.get(i));
        }

        for(int i = 0;i < optionalArgumentCount;i++)
        {
            if(necessaryArgumentCount > 0)
            {
                builder.append(",");
            }
            builder.append(parameters.get(necessaryArgumentCount + i));
            builder.append("=");
            builder.append(optionalArgumentDefaultValue.get(i) + "");
        }

        return builder.toString();
    }

    public static Builder builder()
    {
        return new Builder();
    }

    public static class Builder
    {
        String symbol = "";
        Integer optionalArgumentCount;
        List<MathNode> optionalArgumentDefaultValue;

        Integer necessaryArgumentCount;
        MathNode body;

        List<String> parameters = new ArrayList<>();

        Builder()
        {
            optionalArgumentCount = 0;
            optionalArgumentDefaultValue = new ArrayList<>();
        }

        public Builder symbol(String s)
        {
            this.symbol = Objects.requireNonNull(s);
            return this;
        }

        public Builder optional(Integer count)
        {
            this.optionalArgumentCount = Objects.requireNonNull(count);
            return this;
        }

        public Builder necessary(Integer count)
        {
            this.necessaryArgumentCount = Objects.requireNonNull(count);
            return this;
        }

        public Builder defaultValue(List<MathNode> oadv)
        {
            this.optionalArgumentDefaultValue = Objects.requireNonNull(oadv);
            return this;
        }

        public Builder parameters(List<String> p)
        {
            this.parameters = Objects.requireNonNull(p);
            return this;
        }

        public Builder body(MathNode f)
        {
            this.body = Objects.requireNonNull(f);
            return this;
        }

        public Function build()
        {
            if(symbol == null)
            {
                throw new IllegalStateException("Function symbol must be initialized");
            }
            if(necessaryArgumentCount == null)
            {
                throw new IllegalStateException("Function necessary argument count must be initialized");
            }
            if(body == null)
            {
                throw new IllegalStateException("Function body must be initialized");
            }
            if(parameters == null)
            {
                throw new IllegalStateException("Function parameters must be initialized");
            }

            return new Function(symbol,optionalArgumentCount,necessaryArgumentCount,optionalArgumentDefaultValue,parameters,body);
        }
    }
}

class Constant extends Symbol
{
    double value;

    Constant(String symbol,double value)
    {
        super(symbol);
        this.value = value;
    }

    public static final Constant E = new Constant("e",Math.E);
    public static final Constant PI = new Constant("pi",Math.PI);
    public static final Constant INFINITY = new Constant("infty",Double.POSITIVE_INFINITY);
    public static final Constant I = new Constant("i",Double.NaN);

    public double getValue()
    {
        if(this == I)
        {
            throw new IllegalStateException("The complex unit can't be displayed as a double");
        }
        return value;
    }

    @Override
    public String toString()
    {
        return getClass().getName() + "<value=" + (this == I ? "i" : value) + ">";
    }
}











